Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/项目笔记/代达罗斯/BUG

hook 调用顺序不一致

3 分钟阅读 · Note

目录树 578 篇

            • 表单数据获取&提交需要loading
            • hook 调用顺序不一致
          • 项目待做
          • 性能优化
          • UI设计
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗表单数据获取&提交需要loading同一路径↗「Feature」Introduce Crate as an independent metadata entity[ˈentəti] | 引入 Crate 作为独立元数据实体共同主题↗表单最佳实践指南共同主题↗不使用解构赋值共同主题↗踩坑共同主题↗定义共同主题
  • hook 调用顺序不一致

hook 调用顺序不一致

这是 React 的一条硬性规则:每次渲染时,hooks 的调用数量、调用顺序必须完全一致。

React 内部用一个链表来追踪每个组件的 hooks 状态。它通过调用顺序来匹配每次渲染时哪个 hook,对应哪份状态,而不是通过名称或标识符。一旦某次渲染跳过一个 hook,链表索引就全错了。

出问题前的代码结构是:

  export const CrateDetailPageContent = function () {
    // hooks 1-12: useOne, useList, useBreadcrumb, useUpdate, useDelete,
    //            useEffect, useCallback × 6
    // 总共 12 个 hook 调用

    if (!crate) return <LoadingState className="py-20" />;  // ← 早期 return

    // 第 13 个 hook — 只有 crate 不为 null 时才会执行
    const boundArchetypes = useMemo(() => { ... }, [...]);

    return <div>...</div>;
  };

第一次渲染(数据未加载,crate === null):

  • React 执行 12 个 hooks → 遇到早期 return → 13 号槽位为空

第二次渲染(数据返回,crate !== null):

  • React 执行 12 个 hooks → 跳过早期 return → 执行第 13 个 hook (useMemo)

React 发现第二次渲染的 hook 数量(13)与第一次(12)不一致,直接抛出:

▎ Rendered more hooks than during the previous render.

修复方式

把 useMemo 及它所依赖的普通变量全部移到早期 return 之前:

export const CrateDetailPageContent = function () { // ... 12 hooks ...

// 普通变量(null-safe)
const ext = (crate as Record&lt;string, unknown&gt;) ?? {};

// 第 13 个 hook — 现在每次渲染都执行
const boundArchetypes = useMemo(() =&gt; { ... }, [...]);

// 早期 return 现在在所有 hook 之后,安全
if (!crate) return &lt;LoadingState className="py-20" /&gt;;

return &lt;div&gt;...&lt;/div&gt;;

};

当 crate 为 null 时,useMemo 的结果是一个空数组,值没被用到,但 hook 本身被调用了,链表索引保持了稳定。


以后要注意的点

  1. 早期 return 只能放在最后一个 hook 之后

任何 if (condition) return <JSX /> 的上面都不允许再有 useXxx() 调用。写组件时先铺完所有 hooks,再把条件渲染放在它们下面。

  1. 不要在任何条件分支内调用 hook

// ❌ 错误 if (someCondition) { useEffect(() => { ... }, []); // 第二次渲染可能不执行 }

// ✅ 正确 — 把条件放进 hook 内部 useEffect(() => { if (someCondition) { ... } }, [someCondition]);

  1. 不要在循环内调用 hook

hook 调用次数必须固定。循环的迭代次数可能在渲染间变化。

  1. oxlint 的 react-hooks 规则有助于预防,但不完全可靠

我们项目的 oxlint 配置已经开启了 react-hooks 相关规则。但它的静态分析有时无法追踪到通过变量间接导致的条件分歧(比如 crate 来自 hook 返回值,lint 不一定能推断它何时为 null)。所以最终还是靠代码审查和自觉遵守这条规则。

  1. 写组件时先铺 hooks,后写 JSX 的逻辑顺序是个好习惯

function Component() { // ── 第一层:路由参数、全局状态 ── // ── 第二层:数据获取 hooks(useOne / useList / useQuery)── // ── 第三层:派生计算(useMemo)── // ── 第四层:回调(useCallback)── // ── 第五层:副作用(useEffect)── // ── 第六层:条件渲染 / 早期 return ── ← 永远最后 // ── 第七层:主 JSX ── }

遵循这个分层,早期 return 不可能插到 hook 前面去。